A Brief Discussion on Exception Handling and State Restoration for SaveChanges() in Entity Framework
TLDR
- After
SaveChanges()fails, the state of the Entities is preserved, causing subsequent write operations to include the previously failed changes, which leads to a chain reaction of failures. - It is recommended to override
SaveChanges()andSaveChangesAsync()inDbContext, usingChangeTrackerto catchDbUpdateExceptionand reset the Entity state. - For Entities in the
Modifiedstate, useentry.CurrentValues.SetValues(entry.OriginalValues)to restore data and change the state toUnchanged. - For Entities in the
Addedstate, change the state toDetached. - In complex structures involving foreign key relationships or navigation properties, restoring Entity state may lead to cache inconsistency; this restoration mechanism is not recommended in such scenarios.
InvalidOperationExceptionoccurring during theDbSet.Add()phase (e.g., primary key conflicts) will not be caught by theSaveChanges()error handling mechanism.
Common Exceptions in Entity Framework
During development, understanding the exception types thrown by EF helps in handling errors correctly:
- DbUpdateException: Thrown when an error occurs while saving to the database (e.g., violation of database constraints, connection interruption). This exception usually encapsulates the underlying SQL execution error.
- DbUpdateConcurrencyException: Thrown when a concurrency conflict occurs (e.g.,
RowVersionorConcurrencyCheckis set, but the data in the database has been modified by someone else). - DbEntityValidationException: A validation exception from older versions of EF, which has been removed in EF Core. It is recommended to use Model Binding or a Service Layer for data validation.
Recommendations for Error Message Handling
When do you encounter the issue of overly generic error messages? When the system returns the raw Exception message directly to the frontend.
- If you need to hide details from the outside: You should log the full information of the
InnerExceptionand return only a generic error message to the frontend. - If you do not need to hide details: You can override
SaveChanges()inDbContext, catch the exception, and re-throw a new Exception containing the full error message to facilitate clear accountability.
State Restoration When SaveChanges() Fails
When do you encounter state restoration issues? When developers rely on database primary keys to block duplicate data and do not clear the changes in ChangeTracker after SaveChanges() fails.
Since EF preserves the failed Entity state, if the first SaveChanges() fails, subsequent write operations will still attempt to send the failed data to the database, causing all subsequent operations to fail. If you wish to ignore the changes after a failure, you can manually restore the state by overriding SaveChanges().
Implementation
The following is an extension implementation for DbContext used to automatically reset states when a DbUpdateException occurs:
public partial class TestEFContext {
public override int SaveChanges() {
return SaveChanges(true);
}
public override int SaveChanges(bool acceptAllChangesOnSuccess) {
try {
return base.SaveChanges(acceptAllChangesOnSuccess);
} catch (DbUpdateException ex) {
throw ResetEntityStateAndFixMessage(ex);
}
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) {
return SaveChangesAsync(true, cancellationToken);
}
public override async Task<int> SaveChangesAsync(
bool acceptAllChangesOnSuccess,
CancellationToken cancellationToken = default
) {
try {
return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
} catch (DbUpdateException ex) {
throw ResetEntityStateAndFixMessage(ex);
}
}
private DbUpdateException ResetEntityStateAndFixMessage(DbUpdateException ex) {
ResetEntityStates(ChangeTracker.Entries());
return new DbUpdateException(ex.InnerException.Message, ex);
}
private static void ResetEntityStates(IEnumerable<EntityEntry> entries) {
foreach (EntityEntry entry in entries) {
ResetEntityState(entry);
}
}
private static void ResetEntityState(EntityEntry entry) {
switch (entry.State) {
case EntityState.Added:
entry.State = EntityState.Detached;
break;
case EntityState.Modified:
entry.CurrentValues.SetValues(entry.OriginalValues);
entry.State = EntityState.Unchanged;
break;
case EntityState.Deleted:
entry.State = entry.Entity is Dictionary<string, object>
? EntityState.Detached
: EntityState.Unchanged;
break;
}
}
}WARNING
When using DbSet.Add() to add an Entity that has the same PK as data already queried, an InvalidOperationException will be thrown. Since the exception is thrown during Add() rather than SaveChanges(), it will not be caught by the error handling mechanism mentioned above.
Notes on Foreign Key Relationships
When do you encounter restoration failure issues? When the Entity structure contains complex navigation properties or foreign key relationships.
Tests have shown that if a related Entity in the EntityState.Deleted state is set to Unchanged, it may cause the DbContext caching mechanism to return an incorrect navigation property state. Although setting it to Detached can solve some problems, in general, there is still a potential risk of cache inconsistency when manually restoring Entity states in complex structures involving foreign key relationships.
TIP
A complete executable example for this article: CloudyWing/EfCoreBehaviorSample.
Change Log
- 2024-08-17 Initial version created.
- 2026-05-29 Added link to the corresponding GitHub sample project.